# -*- encoding: utf-8 -*-
"""
# |=========================|
# |    Coding by Noob_0     |
# | Noob_0-main@outlook.com |
# |=========================|

Terminal-for-Windows support

本地依赖:
    threading  # 用于CMDPopen,ThreadsPlus
    subprocess  # 用于CMDPopen
    ctypes  # 用于ThreadsPlus.killall()
第三方库依赖:
    None
架构:
    _Queue类,队列,无依赖
        __init__(lenght=64,*items):初始化
        enqueue(item):入队
        dequeue(n=1):出队
        add(*items):连续入队
        wait():获取更新元素
    Command(str)类,储存命令,无依赖
        __init__(command):初始化
        complete():返回CommandPossible
        completeNow(choose=True):返回CommandPossible中相似度最高的Command对象
    CMDPopen类,创建CMD通信管道,依赖_Queue,Command;subprocess,ThreadsPlus
        __init__(command='cmd.exe',logging=True,log_size=64):初始化,创建CMD通信管道
        command(_command):输入命令
        kill():强制终止
        wait():等待命令返回结果
        log:返回_Queue日志
        isAlive:获取管道是否存活
    TreadsPlus类,创建线程组,依赖threading,ctypes
        __init__(funcs,args,start_now=True):初始化,创建线程组
        run_all():启动线程组
        isrunning(index=None):获取线程组running状态序列
        kill_all():强制终止线程组,依赖ctypes
        status:线程状态序列
        results:线程返回结果序列
    CommandCompleter类,命令补全提示模块,依赖Command
        __init__(commands,times:dict=None,log_size=3):配置CommandCompleter
        complete(command):返回CommandPossible
        isComplete(command):获取Command对象是否存在于已知Commands中
        command_log:历史记录日志

====How-To-Use-support.py====
    ---CMDPopen使用指南---
        使用CMDPopen()创建一个新管道
        ---init:args参数---
            command='cmd.exe':初始化命令,如cmd.exe,powershell等等
            logging=True:是否记录日志,建议开启
            log_size=64:日志缓存长度,logging=False时无效,指缓存输出的队列最大长度,关系不大
        ---method方法---  # 秉持简洁实用的原则,方法不多,按需增加
            <class>.command(_command)输入命令,传入command或str/bytes格式
            <class>.kill()强制终止,终止后不可传入/输出
            <class>.wait()等待命令返回结果,每次返回从上一次开始的所有结果,方便至极!!
        ---attribute属性--
            <class>.log返回一个长度为<log_size>的队列,<class>.log()返回列表  #<class>.log: callable
            <class>.isAlive:bool,代表管道是否存活(kill后.isAlive=False)
    ========
    ---ThreadsPlus使用指南---
        使用ThreadsPlus()创造一个线程集
        ---init:args参数---
            funcs:传入函数,支持传入函数或函数序列(list,tuple),支持exec,eval
            args:传入函数的参数序列,接受Any,建议使用[[<函数1参数>],[<函数2参数>],...]
            start_now:默认True,立即启动线程,False后可以调用run_all方法启动线程
        ---method方法---
            <class>.run_all()启动该线程组的所有线程(未启动情况下)
            <class>.isrunning(index=None)返回一个list[bool]布尔值序列,代表其线程是否正在运行,可以指定线程index,返回bool
            <class>.kill_all()强行结束该线程组的所有线程,慎用!!
        ---attribute属性---
            <class>.status线程状态序列,一般为"not-start","running","stop"
            <class>.results线程返回结果序列,初始值则为list[None],储存线程的返回结果
    ========
    ---附加类-Command---
        ---method方法---
            <class>.complete()返回CommandPossible对象,相当于CommandCompleter.complete(self),请在配置CommandCompleter后使用!!
            <class>.completeNow(choose=True)返回CommandPossible中相似度最高的Command对象,请在配置CommandCompleter后使用!!
                choose=True,开启后使用CommandCompleter.CommandPossible.choose(index)返回对象
    ========
    ---CommandCompleter使用指南---
    ---Not-Complete-未完成---
        ---start配置---
            尚未公开
        ---method方法---  # 请在配置后使用!!
            <class>.complete(command)返回CommandPossible对象,CommandPossible是一个由Command组成的序列对象
                !!请使用CommandPossible.choose(index)选择对象!!
            <class>.isComplete(command)返回bool,用于判断Command对象是否存在于已知Commands中
====end====
"""
import threading  # 用于CMDPopen,ThreadsPlus
import subprocess  # 用于CMDPopen
import ctypes  # 用于ThreadsPlus.kill_all()


# |=========================|
# |     Coded by Noob_0     |
# | Noob_0-main@outlook.com |
# |=========================|
class _Queue:
    def __init__(self, lenght: int = 64, *items):
        if not isinstance(lenght, int):
            raise TypeError('lenght must be an int')
        if lenght <= 0:
            raise ValueError('lenght must > 0')
        if len(items) > lenght:
            lenght = len(items)
        self.index = self.e_index = 0
        self._list_lenght = lenght
        if items:
            if len(items) > lenght:
                items = items[:lenght]
            self._list = list(items) + [None] * (lenght - len(items))
            self.index += len(items) % lenght
            self.q_lenght = len(items)
            self._wait = [0, True]
        else:
            self._list = [None] * lenght
            self.q_lenght = 0
            self._wait = [0, False]

    def enqueue(self, item):
        self._list[self.index] = item
        if self.index == self.e_index and self.q_lenght:
            if self._wait[0] == self.index and self._wait[1]:
                self._wait[0] = (self._wait[0] + 1) % self._list_lenght
            self.index = (self.index + 1) % self._list_lenght
            self.e_index = self.index
        else:
            self.index = (self.index + 1) % self._list_lenght
            self.q_lenght += 1
        self._wait[1] = True

    def dequeue(self, n: int = 1):
        if not isinstance(n, int):
            raise TypeError('n must be an int')
        if n <= 0:
            raise ValueError('n must > 0')
        _list = []
        for i in range(n):
            _list.append(self._list[self.e_index])
            if self.q_lenght == 0:
                break
            else:
                self.e_index = (self.e_index + 1) % self._list_lenght
                self.q_lenght -= 1
        if len(_list) > 1:
            return _list
        else:
            return _list[0]

    def clear(self, item=None):
        self._list = [item] * self._list_lenght
        self.q_lenght = self.index = self.e_index = 0
        self._wait = [0, False]

    def add(self, *items):
        for i in items:
            self.enqueue(i)

    def return_list(self):
        if self.q_lenght == 0:
            return []
        if self.index > self.e_index:
            _l = self._list[self.e_index:self.index][::-1]
        else:
            _l = self._list[:self.index][::-1] + self._list[self.e_index:][::-1]
        return _l

    def wait(self):
        while not self._wait[1]:
            pass
        if self.index >= self._wait[0]:
            _result = self._list[self._wait[0]:self.index]
        else:
            _result = self._list[self._wait[0]:] + self._list[:self.index]
        self._wait = [self.index, False]
        return _result

    def __repr__(self):
        return str(self.return_list())

    def __str__(self):
        return str(self.return_list())

    def __call__(self, *args, **kwargs):
        return self.return_list()


class Command(str):
    def complete(self):
        return CommandCompleter.complete(command=self)

    def completeNow(self, choose=True):
        _result: CommandCompleter.CommandsPossible = CommandCompleter.complete(command=self)
        if choose:
            return _result.choose(0)
        else:
            return _result[0]


class ThreadsPlus:
    def __init__(self, funcs: (tuple, list, str), args: (tuple, list) = tuple(), start_now=True):
        self._stop = [False]
        self._thread_list = []
        if not isinstance(funcs, (tuple, list)):
            funcs = [funcs]
        elif not isinstance(funcs, list):
            funcs = list(funcs)
        if len(funcs) == 1:
            args = [[args]]
        if not isinstance(args, (tuple, list)):
            args = list(map(lambda it: [it], args))
        elif not isinstance(args, list):
            args = list(args)
        for i in range(len(args)):
            if not isinstance(i, (tuple, list)):
                args[i] = [args[i]]
        while len(funcs) > len(args):
            args.append(None)
        self.funcs = funcs
        self.args = args
        self.status = ['not-start'] * len(funcs)
        self.results = [None] * len(funcs)
        self._thread_list = []
        if start_now:
            self.run_all()

    def run_all(self):
        self._stop[0] = False
        threading.Thread(target=self._run, daemon=True).start()

    def _run(self):
        def get_results(_self, _i, target, arg=tuple()):
            results = target(*arg)
            _self.results[_i] = results

        son_thread = []
        for index in range(len(self.funcs)):
            if type(self.funcs[index]) == str:
                if 'eval' in self.funcs[index] and self.funcs[index].index('eval') == 0:
                    def f():
                        _f: str = self.funcs[index][5:]
                        return eval(_f)

                    son_thread.append(threading.Thread(target=get_results, args=(self, index, f), daemon=True))
                else:
                    def f():
                        _f: str = self.funcs[index]
                        return exec(_f)

                    son_thread.append(threading.Thread(target=get_results, args=(self, index, f), daemon=True))
                son_thread[-1].start()
            else:
                if self.args[index] is None:
                    f = self.funcs[index]
                    son_thread.append(threading.Thread(target=get_results, args=(self, index, f), daemon=True))
                else:
                    f = self.funcs[index]
                    args = self.args[index]
                    son_thread.append(threading.Thread(target=get_results, args=(self, index, f, args), daemon=True))
                son_thread[-1].start()
        self._thread_list = son_thread
        while not self._stop[0]:
            pass

        self.status = ['_stop_try'] * len(son_thread)
        print('_stop_try')
        raise TimeoutError('stop_thread')

    def _stop_try(self):
        self._stop[0] = True

    def kill_all(self):
        def terminate_thread(thread):
            if not thread.isAlive():
                return

            exc = ctypes.py_object(SystemExit)
            res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
                ctypes.c_long(thread.ident), exc)
            if res == 0:
                raise ValueError("nonexistent thread id")
            elif res > 1:
                ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)

        for i in self._thread_list:
            terminate_thread(i)

    def isrunning(self, index=None):
        if index is None:
            return tuple(map(lambda obj: True if obj == 'running' else False, self.status))
        else:
            try:
                index = int(index)
            except (TypeError, ValueError):
                return self.isrunning()
            if self.status[index] == 'running':
                return True
            else:
                return False


# wrapper class  装饰器类
class Threaded:
    daemon = True

    class Thread(threading.Thread):
        def __init__(self, target, args, kwargs, daemon=False):
            self.result = None
            self.target = target
            self.args = args
            self.kwargs = kwargs
            super().__init__(target=target, args=args, kwargs=kwargs, daemon=daemon)

        def run(self) -> None:
            self.result = self.target(*self.args, **self.kwargs)

    class ThreadedResult(object):
        def __init__(self, thread):
            self.thread: Threaded.Thread = thread
            self.thread.start()

        def __call__(self):
            return self.result

        def __repr__(self):
            return self.result

        def __int__(self):
            return int(self.result)

        def __float__(self):
            return float(self.result)

        def __str__(self):
            return str(self.result)

        def __bytes__(self):
            return bytes(self.result)

        @property
        def result(self):
            return self.thread.result

    def __init__(self, func):  # 接受函数
        self.func = func

    def __call__(self, *func_args, **func_kwargs):  # 返回函数
        thread = self.Thread(target=self.func, args=func_args, kwargs=func_kwargs, daemon=self.daemon)
        return self.ThreadedResult(thread=thread)


class CMDPopen:
    def __init__(self, command='cmd.exe', logging=True, log_size=64):
        # command:初始命令,如cmd.exe,powershell等等;logging:是否记录日志;log_size:日志缓存长度
        self.popen = subprocess.Popen(str(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE, cwd='C:\\')
        self._son_thread = []
        self.logging = logging
        if logging:
            self.log = _Queue(lenght=log_size)
        self.stdout = self._stdout_logging
        self.stderr = self._stderr_logging
        self.isAlive = True

    @Threaded
    def _stdout_logging(self):
        try:
            for line in iter(self.popen.stdout.readline, b''):
                if line:
                    r = line.decode(encoding='GBK', errors='ignore').strip()
                    if self.logging:
                        self.log.enqueue(r)
        finally:
            return

    @Threaded
    def _stderr_logging(self):
        try:
            for line in iter(self.popen.stderr.readline, b''):
                if line:
                    r = line.decode(encoding='GBK', errors='ignore').strip()
                    if self.logging:
                        self.log.enqueue(r)
        finally:
            return

    def command(self, _command: (Command, str, bytes)):
        if isinstance(_command, bytes):
            self.popen.stdin.write(_command.strip() + b'\r\n')
            self.popen.stdin.flush()
            return
        else:
            if not isinstance(_command, Command):
                _command = Command(_command)
            self.popen.stdin.write(_command.encode('utf-8') + b'\r\n')
            self.popen.stdin.flush()

    def kill(self):
        self.popen.kill()
        self.isAlive = False

    def wait(self):
        return self.log.wait()


class CommandCompleter:
    commands: set = set()
    times = {}
    command_log: _Queue = _Queue(3)

    class CommandsPossible(tuple):
        def choose(self, index: int):
            _result = self[index]
            CommandCompleter.times[_result] += 1
            CommandCompleter.command_log.enqueue(_result)
            return _result

    @classmethod
    def __init__(cls, commands: (list, tuple), times: dict = None, log_size: int = 3):
        if times is None:
            times = dict()
        commands = cls._extract(commands)
        cls.commands = set(map(lambda obj: Command(obj), commands))
        for i in cls.commands:
            cls.times[i] = 0
        for i in times:
            cls.times[i] = times[i]
        cls.command_log = _Queue(log_size)
        cls.command_log.clear(item='')
        cls.command_log.q_lenght = log_size

    @staticmethod
    #  --Not-Completed--
    def _commands_format(commands: (list, tuple)):
        if isinstance(commands, (list, tuple)):
            raise TypeError('commands must be a list or tuple')
        commands = list(commands)
        return commands

    @staticmethod
    #  --Not-Completed--
    def _extract(commands: (list, tuple)):
        if isinstance(commands, str):
            pass
        else:
            commands = list(commands)
            pass
            return commands

    @staticmethod
    # deploy展开,from Noob_0
    def _deploy(self, deploy_type: (type, tuple, list) = (list, tuple), depth: int = -1):  # deploy展开
        def deploy_main(s, dep):
            if isinstance(s, deploy_type):
                _l: list = list()
                for j in s:
                    _l += deploy_main(j, dep - 1)
                return _l
            else:
                return [s]

        return deploy_main(self, depth)

    @classmethod
    def complete(cls, command):
        command_log = cls.command_log()
        times = cls.times
        result_list = []
        if not isinstance(command, Command):
            command = Command(command)
        for k in cls.commands:
            if command in k:
                n = k.index(command)
                if n >= len(result_list):
                    for i in range(n - len(result_list) + 1):
                        result_list.append([])
                result_list[n].append(k)
        for ki, k in enumerate(result_list):
            k.sort()
            for _index in range(len(k) - 1):
                i_max = [_index, _index]
                for i in k[_index:]:
                    if i in command_log:
                        if k[i_max[1]] in command_log:
                            if command_log.index(i) < command_log.index(k[i_max[1]]):
                                i_max[1] = k.index(i)
                        else:
                            i_max[1] = k.index(i)
                    else:
                        if (k[i_max[1]] not in command_log) and (times[i] > times[k[i_max[1]]]):
                            i_max[1] = k.index(i)
                k[i_max[0]], k[i_max[1]] = k[i_max[1]], k[i_max[0]]
            result_list[ki] = k
        return cls.CommandsPossible(cls._deploy(result_list))

    @classmethod
    def isComplete(cls, command):
        if not isinstance(command, Command):
            command = Command(command)
        if command in cls.commands:
            return True
        else:
            return False


# |=========================|
# |     Coded by Noob_0     |
# | Noob_0-main@outlook.com |
# |=========================|
if __name__ == '__main__':
    import unittest
    from time import sleep


    class SupportTest(unittest.TestCase):
        def test_Command(self):
            command1 = Command('help')
            self.assertEqual(type(command1), Command)

        def test_CMDPopen(self):
            c1 = Command('help')
            c2 = 'help'
            c3 = b'help'
            p1 = CMDPopen(log_size=1024)
            self.assertEqual(p1.wait(), ['Microsoft Windows [版本 6.1.7601]',
                                         '版权所有 (c) 2009 Microsoft Corporation。保留所有权利。',
                                         ''])
            p1.command(c1)
            sleep(0.1)
            _result1 = p1.wait()
            self.assertEqual(len(_result1) > 0, True)
            p1.command(c2)
            sleep(0.1)
            _result2 = p1.wait()
            self.assertEqual(_result1, _result2)
            p1.command(c3)
            sleep(0.1)
            _result3 = p1.wait()
            self.assertEqual(_result2, _result3)

        def test_CommandCompleter(self):
            # --配置部分--
            CommandCompleter(['commandA', 'commandB', 'commandC'])
            #  --end--
            self.assertEqual(CommandCompleter.complete('c'), ('commandA', 'commandB', 'commandC'))


    unittest.main()
